<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h4>原始音频</h4>
    <p>段落1：<audio src="./assets/1.wav" controls></audio></p>
    <p>段落2：<audio src="./assets/2.wav" controls></audio></p>
    <p>背景音：<audio src="./assets/bgmusic.wav" controls></audio></p>
    <h4>操作与预览</h4>
    <button id="audioConcat">拼接两个段落音频</button>
    <div id="outputConcat" class="output"></div>

    <button id="audioMerge" disabled>合并音频</button>
    <div id="outputMerge" class="output"></div>
    <script>
      // 音频地址
      const audioSrc = ['./assets/1.wav', './assets/2.wav'];
      const bgAudioSrc = './assets/bgmusic.wav';

      // AudioContext
      const audioContext = new AudioContext();
      // 基于src地址获得 AudioBuffer 的方法
      const getAudioBuffer = (src) => {
        return new Promise((resolve, reject) => {
          fetch(src)
            .then((response) => response.arrayBuffer())
            .then((arrayBuffer) => {
              audioContext.decodeAudioData(arrayBuffer).then((buffer) => {
                resolve(buffer);
              });
            });
        });
      };

      // 拼接音频的方法
      const concatAudio = (arrBufferList) => {
        // 获得 AudioBuffer
        const audioBufferList = arrBufferList;
        // 最大通道数
        const maxChannelNumber = Math.max(
          ...audioBufferList.map((audioBuffer) => audioBuffer.numberOfChannels)
        );
        // 总长度
        const totalLength = audioBufferList
          .map((buffer) => buffer.length)
          .reduce((lenA, lenB) => lenA + lenB, 0);

        // 创建一个新的 AudioBuffer
        const newAudioBuffer = audioContext.createBuffer(
          maxChannelNumber,
          totalLength,
          audioBufferList[0].sampleRate
        );
        // 将所有的 AudioBuffer 的数据拷贝到新的 AudioBuffer 中
        let offset = 0;

        audioBufferList.forEach((audioBuffer, index) => {
          for (
            let channel = 0;
            channel < audioBuffer.numberOfChannels;
            channel++
          ) {
            newAudioBuffer
              .getChannelData(channel)
              .set(audioBuffer.getChannelData(channel), offset);
          }

          offset += audioBuffer.length;
        });

        return newAudioBuffer;
      };

      // 合并音频的方法
      const mergeAudio = (arrBufferList) => {
        // 获得 AudioBuffer
        const audioBufferList = arrBufferList;
        // 最大播放时长
        const maxDuration = Math.max(
          ...audioBufferList.map((audioBuffer) => audioBuffer.duration)
        );
        // 最大通道数
        const maxChannelNumber = Math.max(
          ...audioBufferList.map((audioBuffer) => audioBuffer.numberOfChannels)
        );
        // 创建一个新的 AudioBuffer
        const newAudioBuffer = audioContext.createBuffer(
          maxChannelNumber,
          audioBufferList[0].sampleRate * maxDuration,
          audioBufferList[0].sampleRate
        );
        // 将所有的 AudioBuffer 的数据合并到新的 AudioBuffer 中
        audioBufferList.forEach((audioBuffer, index) => {
          for (
            let channel = 0;
            channel < audioBuffer.numberOfChannels;
            channel++
          ) {
            const outputData = newAudioBuffer.getChannelData(channel);
            const bufferData = audioBuffer.getChannelData(channel);

            for (
              let i = audioBuffer.getChannelData(channel).length - 1;
              i >= 0;
              i--
            ) {
              outputData[i] += bufferData[i];
            }

            newAudioBuffer.getChannelData(channel).set(outputData);
          }
        });

        return newAudioBuffer;
      };

      // 这个背景音乐合并的时候需要，所以放外面了
      let concatAudioBuffer = null;
      // 点击按钮后
      audioConcat.onclick = async function () {
        if (this.classList.contains('loading')) {
          return;
        }
        this.classList.add('loading');

        // 拼接音频
        const arrBufferList = await Promise.all(
          audioSrc.map((src) => getAudioBuffer(src))
        );

        concatAudioBuffer = concatAudio(arrBufferList);
        const newAudioSrc = URL.createObjectURL(
          bufferToWave(concatAudioBuffer, concatAudioBuffer.length)
        );

        outputConcat.innerHTML = `<audio src="${newAudioSrc}" controls></audio>`;

        // 移除 loading 样式
        this.classList.remove('loading');

        // 移除合并按钮的禁用态
        audioMerge.disabled = false;
      };
     
      // 合并按钮点击后
      audioMerge.onclick = async function () {
        if (!concatAudioBuffer || this.classList.contains('loading')) {
          return;
        }
        this.classList.add('loading');

        // 请求背景音乐的 buffer 数据
        const bgAudioBuffer = await getAudioBuffer(bgAudioSrc);

        // 合并音频
        const newAudioBuffer = mergeAudio([concatAudioBuffer, bgAudioBuffer]);

        // 合并音频
        const newAudioSrc = URL.createObjectURL(
          bufferToWave(newAudioBuffer, newAudioBuffer.length)
        );

        // 预览合并后的音频
        outputMerge.innerHTML = `<audio src="${newAudioSrc}" controls></audio>`;

        // 移除 loading 样式
        this.classList.remove('loading');
      };

      // AudioBuffer 转 blob
      function bufferToWave(abuffer, len) {
        var numOfChan = abuffer.numberOfChannels,
          length = len * numOfChan * 2 + 44,
          buffer = new ArrayBuffer(length),
          view = new DataView(buffer),
          channels = [],
          i,
          sample,
          offset = 0,
          pos = 0;

        // write WAVE header
        // "RIFF"
        setUint32(0x46464952);
        // file length - 8
        setUint32(length - 8);
        // "WAVE"
        setUint32(0x45564157);
        // "fmt " chunk
        setUint32(0x20746d66);
        // length = 16
        setUint32(16);
        // PCM (uncompressed)
        setUint16(1);
        setUint16(numOfChan);
        setUint32(abuffer.sampleRate);
        // avg. bytes/sec
        setUint32(abuffer.sampleRate * 2 * numOfChan);
        // block-align
        setUint16(numOfChan * 2);
        // 16-bit (hardcoded in this demo)
        setUint16(16);
        // "data" - chunk
        setUint32(0x61746164);
        // chunk length
        setUint32(length - pos - 4);

        // write interleaved data
        for (i = 0; i < abuffer.numberOfChannels; i++)
          channels.push(abuffer.getChannelData(i));

        while (pos < length) {
          // interleave channels
          for (i = 0; i < numOfChan; i++) {
            // clamp
            sample = Math.max(-1, Math.min(1, channels[i][offset]));
            // scale to 16-bit signed int
            sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0;
            // write 16-bit sample
            view.setInt16(pos, sample, true);
            pos += 2;
          }
          // next source sample
          offset++;
        }

        // create Blob
        return new Blob([buffer], { type: 'audio/wav' });

        function setUint16(data) {
          view.setUint16(pos, data, true);
          pos += 2;
        }

        function setUint32(data) {
          view.setUint32(pos, data, true);
          pos += 4;
        }
      }
    </script>
  </body>
</html>
